﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace InitializerExtension
{
  class Program
  {
    private static bool _debug = true;

    private static int varCpt = 1;

    static void Main(params string[] parameters)
    {
      if (!(parameters.Length > 0 && parameters.Length < 3 && parameters[0].EndsWith(".sln") && File.Exists(parameters[0])))
        throw new ArgumentException();

      if (parameters.Length > 1)
        _debug = parameters[1] != "-d";

      TransformSolution(Path.GetFullPath(parameters[0]));

      Console.ReadLine();
    }

    private static void TransformSolution(string solutionPath)
    {
      string solutionDirectory = Path.GetDirectoryName(solutionPath);
      string tmpPathBase = Path.GetTempPath() + solutionDirectory.Substring(solutionDirectory.LastIndexOf(@"\") + 1);
      string tmpPath = tmpPathBase;
      string solutionFileName = Path.GetFileName(solutionPath);
      solutionDirectory += @"\";

      int i = 1;
      while (Directory.Exists(tmpPath))
        tmpPath = tmpPathBase + (i++).ToString();
      Directory.CreateDirectory(tmpPath);
      tmpPath += @"\";
      File.Copy(solutionPath, tmpPath + solutionFileName);

      var projects = new List<string>();
      using (var solutionReader = new StreamReader(solutionPath))
      {
        string line;
        while ((line = solutionReader.ReadLine()) != null)
        {
          if (line.StartsWith("Project"))
            projects.Add(line.Split(',')[1].TrimStart().TrimEnd().Replace("\"", ""));
        }
      }
      var compiledProjects = new List<string>();
      int projectIndex = -1;
      while (projects.Count != 0)
      {
        projectIndex = (projectIndex + 1) % projects.Count;
        string solutionProject = solutionDirectory + projects[projectIndex];
        XNamespace ns = XDocument.Load(solutionProject).Root.Attribute("xmlns").Value as string;
        var solutionReferencesProjects = from c in XElement.Load(solutionProject).Elements(ns + "ItemGroup").Elements(ns + "ProjectReference")
                                         select (string)c.Attribute("Include");
        if (solutionReferencesProjects.Count() == 0)
        {
          TransformProjects(solutionDirectory, tmpPath, projects[projectIndex]);
          compiledProjects.Add(Path.GetFullPath(projects[projectIndex]));
          projects.RemoveAt(projectIndex--);
        }
        else
        {
          bool allReferencedCompiled = true;
          foreach (string referecedProject in solutionReferencesProjects)
            if (!compiledProjects.Contains(Path.GetFullPath(Path.GetDirectoryName(projects[projectIndex]) + @"\" + referecedProject)))
            {
              allReferencedCompiled = false;
              break;
            }
          if (allReferencedCompiled)
          {
            TransformProjects(solutionDirectory, tmpPath, projects[projectIndex]);
            compiledProjects.Add(Path.GetFullPath(projects[projectIndex]));
            projects.RemoveAt(projectIndex--);
          }
        }
      }
    }

    private static void TransformProjects(string solutionPath, string solutionTmpPath, string projectRelativePath)
    {
      CreateParentDirectories(solutionTmpPath, projectRelativePath);

      string tmpProjectFilePath = solutionTmpPath + projectRelativePath;
      File.Copy(solutionPath + projectRelativePath, tmpProjectFilePath);

      string tmpProjectDirectory = Path.GetDirectoryName(tmpProjectFilePath) + @"\";
      string projectDirectory = Path.GetDirectoryName(solutionPath + projectRelativePath) + @"\";

      XNamespace ns = XDocument.Load(tmpProjectFilePath).Root.Attribute("xmlns").Value as string;
      var csFiles = from c in XElement.Load(tmpProjectFilePath).Elements(ns + "ItemGroup").Elements(ns + "Compile")
                    where ((string)c.Attribute("Include")).EndsWith(".cs")
                    select (string)c.Attribute("Include");

      var referenceAssemblies = from c in XElement.Load(tmpProjectFilePath).Elements(ns + "ItemGroup").Elements(ns + "Reference")
                                select new
                                       {
                                         Name = (string)c.Attribute("Include") + ".dll",
                                         HintPath = (string)c.Element(ns + "HintPath"),
                                         RequiredTargetFramework = (string)c.Element(ns + "RequiredTargetFramework")
                                       };

      var projectReferenceAssemblies = from c in XElement.Load(tmpProjectFilePath).Elements(ns + "ItemGroup").Elements(ns + "ProjectReference")
                                       select (string)c.Attribute("Include");

      var referencesAssembliesPath = new string[referenceAssemblies.Count() + projectReferenceAssemblies.Count()];
      int referenceIndex = 0;
      foreach (var referenceAssembly in referenceAssemblies)
      {
        if (!String.IsNullOrEmpty(referenceAssembly.HintPath))
          referencesAssembliesPath[referenceIndex++] = Path.GetFullPath(projectDirectory + referenceAssembly.HintPath);
        //else if (!String.IsNullOrEmpty(referenceAssembly.RequiredTargetFramework))
        //  referencesAssembliesPath[referenceIndex++] = @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v" +
        //    referenceAssembly.RequiredTargetFramework + @"\" + referenceAssembly.Name;
        else
          referencesAssembliesPath[referenceIndex++] = referenceAssembly.Name;
      }
      foreach (var projectReferenceAssembly in projectReferenceAssemblies)
      {
        string csprojFile = Path.GetFullPath(tmpProjectDirectory + projectReferenceAssembly);
        string csprojBinFile = Path.GetFileName(csprojFile);
        csprojBinFile = Path.GetDirectoryName(csprojFile) + @"\bin\" + csprojBinFile.Substring(0, csprojBinFile.IndexOf(".csproj"));

        XNamespace nsProjRefAss = XDocument.Load(csprojFile).Root.Attribute("xmlns").Value as string;
        bool exeProjRefAss = (from c in XElement.Load(csprojFile).Elements(nsProjRefAss + "PropertyGroup")
                              select (string)c.Element(nsProjRefAss + "OutputType")).First() == "exe";

        referencesAssembliesPath[referenceIndex++] = csprojBinFile + (exeProjRefAss ? ".exe" : ".dll");
      }

      Directory.CreateDirectory(tmpProjectDirectory + "bin");

      var tmpFiles = new List<string>();

      foreach (string csFile in csFiles)
      {
        CreateParentDirectories(tmpProjectDirectory, csFile);
        File.Copy(projectDirectory + csFile, tmpProjectDirectory + csFile);
        tmpFiles.Add(tmpProjectDirectory + csFile);
      }

      bool exe = (from c in XElement.Load(tmpProjectFilePath).Elements(ns + "PropertyGroup")
                  select (string)c.Element(ns + "OutputType")).First() == "Exe";

      var provider = new CSharpCodeProvider(new Dictionary<string, string>() { { "CompilerVersion", "v3.5" } });

      string projectName = Path.GetFileName(tmpProjectFilePath).Substring(0, Path.GetFileName(tmpProjectFilePath).IndexOf(".csproj"));
      var cp = new CompilerParameters(referencesAssembliesPath, tmpProjectDirectory + @"bin\" + projectName +
                 (exe ? ".exe" : ".dll"), true) { GenerateExecutable = exe, IncludeDebugInformation = _debug };

      CompilerResults cr;

      int indexError = 0;

      string errorLine = null;
      bool changed = false;
      while ((cr = provider.CompileAssemblyFromFile(cp, tmpFiles.ToArray())).Errors.Count > indexError)
      {
        var error = cr.Errors[indexError];
        var lines = new List<string>();
        bool ko = false;
        using (var fileReader = new StreamReader(error.FileName))
        {
          string line;
          while ((line = fileReader.ReadLine()) != null)
            lines.Add(line); // On considère que le fichier ne sera jamais énorme.
          string possibleSpaces = "[ ]*";
          string spaces = "[ ]+";
          string type = possibleSpaces + "[A-Za-z0-9]+" + spaces;
          string elt = possibleSpaces + @"[^,]+(\(*\))*(\{*\})*[^,]*" + possibleSpaces;
          string property = possibleSpaces + "[A-Za-z0-9]+" + possibleSpaces;
          int errorLineIndex = error.Line - 1;
          if (errorLine == lines[errorLineIndex] && !changed)
          {
            indexError++;
            ko = true;
          }
          else
          {
            errorLine = lines[errorLineIndex];
            changed = false;
            if (Regex.IsMatch(errorLine, ";" + possibleSpaces + "$"))
            {
              if (errorLine.IndexOf(';') == errorLine.LastIndexOf(';'))
              {
                string linePrec = lines[errorLineIndex - 1];
                if (!(errorLineIndex == 0 || linePrec.Length == 0 || linePrec.TrimStart()[0] == '#' ||
                  (linePrec.TrimEnd().Length - 1 == linePrec.LastIndexOf(')') &&
                    linePrec.Intersect("(").Count() < linePrec.Intersect(")").Count()) ||
                  (Regex.IsMatch(linePrec, ";" + possibleSpaces + "$") || Regex.IsMatch(lines[errorLineIndex - 1], "{" + possibleSpaces + "$"))))
                {
                  lines[errorLineIndex - 1] += errorLine;
                  lines.RemoveAt(errorLineIndex);
                  changed = true;
                }
                else
                {
                  int pars = 0;
                  int firstIndexOfParOpenedNotClosed = 0;
                  for (int indexChar = 0; indexChar < error.Column; indexChar++)
                    switch (errorLine[indexChar])
                    {
                      case '(':
                        if (firstIndexOfParOpenedNotClosed == 0)
                          firstIndexOfParOpenedNotClosed = indexChar;
                        pars++;
                        break;
                      case ')':
                        pars--;
                        if (pars == 0)
                          firstIndexOfParOpenedNotClosed = 0;
                        break;
                    }
                  if (pars > 0)
                  {
                    int lastIndexOfParClosed = 0;
                    for (int indexChar = error.Column; indexChar < errorLine.Length && pars != 0; indexChar++)
                      switch (errorLine[indexChar])
                      {
                        case '(':
                          pars++;
                          break;
                        case ')':
                          lastIndexOfParClosed = indexChar;
                          pars--;
                          break;
                      }
                    string mmCS31Line = Regex.Match(errorLine.Substring(firstIndexOfParOpenedNotClosed + 1, lastIndexOfParClosed - firstIndexOfParOpenedNotClosed), elt + ":" + possibleSpaces +
                    "{" + property + "=" + elt + "(," + property + "=" + elt + ")*" + "}" + possibleSpaces).Value;
                    if (! string.IsNullOrEmpty(mmCS31Line))
                    {
                      string var = "mmCS31" + (varCpt++).ToString();
                      lines[errorLineIndex] = errorLine.Replace(mmCS31Line, var);
                      lines.Insert(errorLineIndex, "var " + var + " = " + mmCS31Line + ";");
                      lines.Insert(errorLineIndex, "{");
                      lines.Insert(errorLineIndex + 3, "}");
                    }
                  }
                }
              }
              else
              {
                string subLine = errorLine;
                int indexPointComma;
                string subLineToInsert = subLine;
                while ((indexPointComma = subLine.IndexOf(';')) != -1)
                {
                  subLineToInsert = subLine.Substring(0, ++indexPointComma);
                  int pars = 0;
                  for (int indexChar = 0; indexChar < subLineToInsert.Length; indexChar++)
                    switch (subLineToInsert[indexChar])
                    {
                      case '(':
                        pars++;
                        break;
                      case ')':
                        if (pars == 0)
                        {
                          string subSubLineToInsert = subLineToInsert.Substring(0, ++indexChar);
                          Accolades(lines, subSubLineToInsert, ref errorLineIndex);
                          subLineToInsert = subLineToInsert.Substring(indexChar);
                          indexChar = 0;
                        }
                        else
                          pars--;
                        break;
                    }
                  Accolades(lines, subLineToInsert, ref errorLineIndex);
                  subLine = subLine.Substring(indexPointComma);
                }
                lines.Insert(errorLineIndex++, subLine);
                lines.RemoveAt(errorLineIndex);
                changed = true;
              }
            }
            else
            {
              if (lines.Count > errorLineIndex + 1)
              {
                lines[errorLineIndex] += lines[++errorLineIndex];
                lines.RemoveAt(errorLineIndex);
                changed = true;
              }
            }
            if (!changed)
            {
              if (Regex.IsMatch(errorLine, "^(" + type + "){0,1}" + elt + "=" + elt + ":" + possibleSpaces +
                "{" + property + "=" + elt + "(," + property + "=" + elt + ")*" + "}" + possibleSpaces + ";" + possibleSpaces + "$"))
              {
                int index = errorLineIndex + 1;
                bool endWithPar = lines[errorLineIndex - 1].TrimEnd().EndsWith(")");
                if (endWithPar)
                  lines.Insert(index++, "{");
                lines.Insert(index++, errorLine.Substring(0, error.Column - 1).TrimEnd(' ') + ";");
                index = SetProperties(errorLine, error, lines, possibleSpaces, errorLineIndex, index, endWithPar);
              }
              else if (Regex.IsMatch(errorLine, "^" + elt + ":" + possibleSpaces +
                "{" + property + "=" + elt + "(," + property + "=" + elt + ")*" + "}" + possibleSpaces + ";" + possibleSpaces + "$"))
              {
                int index = errorLineIndex + 1;
                bool endWithPar = lines[errorLineIndex - 1].TrimEnd().EndsWith(")");
                if (endWithPar)
                  lines.Insert(index++, "{");
                index = SetProperties(errorLine, error, lines, possibleSpaces, errorLineIndex, index, endWithPar);
              }
            }
          }
        }

        if (!ko)
          using (var fileWriter = new StreamWriter(error.FileName))
          {
            foreach (string line in lines)
              fileWriter.WriteLine(line);
          }
      }

      if (cr.Errors.Count > 0)
        foreach (var error in cr.Errors)
          Console.WriteLine(error);
      else
      {
        // TODO : Copier le fichier de Temp vers le bon répertoire
        var generatedFile = solutionPath + Path.GetDirectoryName(projectRelativePath) + @"\bin\" +
          (_debug ? @"Debug\" : @"Release\") + Path.GetFileName(cp.OutputAssembly);
        File.Delete(generatedFile);
        File.Copy(cp.OutputAssembly, generatedFile);
        Console.WriteLine(projectName + " -> " + generatedFile);
      }
      Console.WriteLine("");
    }

    private static int SetProperties(string errorLine, CompilerError error, List<string> lines, string possibleSpaces, int errorLineIndex, int index, bool endWithPar)
    {
      string[] propertiesInitialization = new string[0];
      var words = errorLine.Substring(0, errorLine.IndexOf("=")).Trim().Split(' ');
      string variable = words[0];
      if (words.Length > 1)
      {
        if (words[1] == ":")
        {
          variable = words[0];
          propertiesInitialization = errorLine.Substring(errorLine.IndexOf(":") + 1).Trim(' ', '{', '}', ';').Split(',');
        }
        else
        {
          variable = words[words.Length - 1];
          propertiesInitialization = errorLine.Substring(error.Column).Trim(' ', '{', '}', ';').Split(',');
        }
      }
      var spacesToStart = Regex.Match(errorLine, "^" + possibleSpaces);
      string startLine = (spacesToStart.Success ? spacesToStart.Value : "") + variable;
      foreach (string propertyInitialization in propertiesInitialization)
        lines.Insert(index++, startLine + "." + propertyInitialization.Trim() + ";");
      if (endWithPar)
        lines.Insert(index++, "}");
      lines.RemoveAt(errorLineIndex);
      return index;
    }

    private static void Accolades(List<string> lines, string line, ref int lineIndex)
    {
      line = line.TrimStart();
      while (line.Length > 0)
        switch (line[0])
        {
          case '{':
          case '}':
            lines.Insert(lineIndex++, line[0].ToString());
            line = line.Substring(1).TrimStart();
            break;
          default:
            lines.Insert(lineIndex++, line);
            return;
        }
    }

    private static void CreateParentDirectories(string directoryBasePath, string relativePath)
    {
      string directoryTmp = directoryBasePath;
      var subDirectories = relativePath.Split('\\');
      for (int directoryLevel = 0; directoryLevel < subDirectories.Length - 1; directoryLevel++)
      {
        directoryTmp = directoryTmp + subDirectories[directoryLevel] + @"\";
        Directory.CreateDirectory(directoryTmp);
      }
    }

    private static void TransformFile(StreamReader cs3ExtentedFile, StreamWriter cs3File)
    {
    }

    //private static string TransformLine(string instruction)
    //{

    //}
  }
}
